/**
 * 
 */
package org.stars.daostars.core.parser;

import static org.stars.daostars.core.SqlFragmentType.TYPE_ARRAY;
import static org.stars.daostars.core.SqlFragmentType.TYPE_IF;

import org.stars.dao.exception.DaoException;
import org.stars.daostars.core.SqlFragment;
import org.stars.daostars.core.SqlFragmentType;
import org.stars.daostars.core.SqlParameter;
import org.stars.daostars.core.SqlParameterType;
import org.stars.daostars.core.SqlQuery;

import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.stars.util.StringUtility;
import java.util.ArrayList;
import java.util.List;

/**
 * Enhanced parser. Il funzionamento è presto detto:
 * 
 * Ricerca tutte le parentesi graffe e le sostituisce con una sequenza di
 * caratteri del tipo
 * 
 * @author Francesco Benincasa (908099)
 * 
 */
public abstract class SqlParser {

	/**
	 * prefisso dei parametri recuperati da appsettings.
	 */
	private static final String CONFIG_PARAMETER_PREFIX = "config.";

	/**
	 * prefisso dei parametri recuperati dallo user.
	 */
	private static final String USER_PARAMETER_PREFIX = "user.";

	/**
	 * Pattern effettuato per la ricerca
	 * 
	 * @return
	 * 		pattern
	 */
	public static String getRegExMatcher() {
		return regIfElse + "|" + regArrayElse + "|" + regParameter + "|"
				+ regVariable;
	}

	/**
	 * parser
	 */
	public final static String regNome = "[\\w\\.\\-_]+";

	/**
	 * regEx per uno statement contenuto tra { }
	 */
	public final static String regPlaceHolder = "\\s*\\<\\[\\[([^]}]*)\\]\\]\\>";

	public final static String regIfElse = "((\\&)\\s*(" + regNome + ")"
			+ regPlaceHolder + "(" + regPlaceHolder + ")?)";

	public final static String regParameter = "((\\&)\\[\\s*(\\s*(" + regNome
			+ ")\\s*)(\\((\\s*(" + regNome + ")\\s*)\\))?\\s*\\])";

	public final static String regVariable = "((\\#)\\[\\s*(\\s*(" + regNome
			+ ")\\s*)(\\((\\s*(" + regNome + ")\\s*)\\))?\\s*\\])";

	public final static String regArrayElse = "((\\@)\\[\\s*(\\s*(" + regNome
			+ ")\\s*)(\\((\\s*(" + regNome + ")\\s*)\\))?\\s*\\]" + "("
			+ regPlaceHolder + ")?)";

	/**
	 * Dato un codice sql plain, questo metodo restituisce un array di clauses.
	 * 
	 * @param sql
	 * @return <code>ArrayList</code> delle clausole sql
	 * @throws Exception
	 */
	public static List<SqlFragment> parse(String sql) throws Exception {
		List<SqlFragment> ret = null;
					
		String reg = getRegExMatcher();

		List<String> aSubQuery = new ArrayList<String>();
		ret = parseString(sql, reg, "", aSubQuery);

		return ret;
	}
	
	/**
	 * Data una stringa sql restituisce una query, al cui interno ritroviamo già
	 * la suddivisione in clausole. Gli attributi definiti sono
	 * <ul>
	 * 		<li>Clauses</li>
	 * 		<li>PlainSql</li>
	 * </ul>
	 * 
	 * @param sql
	 * 			codice sql in input
	 * @return
	 * 			query
	 * @throws Exception
	 * 			in caso di errore
	 */
	public static SqlQuery parseQuery(String sql) throws Exception
	{
		SqlQuery query=new SqlQuery();
		
		List<SqlFragment> array=parse(sql);
		
		query.setClauses(array);
		query.setPlainSql(sql);
		
		return query;
	}


	/**
	 * Dato un array di codice sql e una stringa con *forse* un indice,
	 * restutisce la stringa associata a quell'elemento. Nel caso in cui index
	 * sia non valido o sia oltre la dimensione massima dell'array, restituisce
	 * "".
	 * 
	 * @param array
	 *            array di codice sql
	 * @param sIndex
	 *            stringa contenente l'indice
	 * @return codice sql
	 */
	private static String getStatement(List<String> array, String sIndex) {
		String ret = "";
		if (StringUtility.isInteger(sIndex)) {
			int index = Integer.parseInt(sIndex);

			if (index < array.size()) {
				ret = array.get(index);
			}

		}
		return ret;

	}

	private static List<SqlFragment> parseString(String sql,
			String sPattern, String sDefaultReplaceString,
			List<String> aSubQuery) throws Exception {

		// definiamo i place holder per le sotto query dinamiche
		sql = PlaceHolderFinder.execute(sql, aSubQuery);
		 
		List<SqlFragment> parsedQuery = new ArrayList<SqlFragment>();
		SqlFragment currentClause = null;
		String sReplaceString;

		// compiliamo il matcher per i parametri della query
		Pattern patternBegin = Pattern.compile(sPattern, Pattern.MULTILINE);
		Matcher matcher = patternBegin.matcher(sql);
		StringBuffer sb = new StringBuffer();
		String sTemp;

		while (matcher.find()) {
			sReplaceString = "";		
			// nel caso di clausola condizionale ne creiamo una
			if (currentClause == null
					|| currentClause.getType() != SqlFragmentType.TYPE_SIMPLE) {
				currentClause = new SqlFragment();
				parsedQuery.add(currentClause); // la aggiungiamo alla
				// definizione della query
			}

			if (matcher.group(RegExGroup.GROUP_IF) != null) {
				// -----------------------------------------------
				// clausola condizionale
				// -----------------------------------------------

				sReplaceString = sDefaultReplaceString;
				String sNome = StringUtility.nvl(
						matcher.group(RegExGroup.GROUP_IF)).trim();
				String sIndexIf = StringUtility.nvl(matcher
						.group(RegExGroup.GROUP_IF_STATEMENT));
				String sIndexElse = StringUtility.nvl(matcher
						.group(RegExGroup.GROUP_IF_STATEMENT_ELSE));

				// prima di creare la clausola condizionata, la portiamo fuori.
				matcher.appendReplacement(sb, "");
				sTemp = sb.toString(); // rimuoviamo i caratteri di invio etc
				// etc
				currentClause.setSql(currentClause.getSql(null) + sTemp + "");
				sb = new StringBuffer();

				currentClause = new SqlFragment(sNome);
				List<String> aSubQueryIf=new ArrayList<String>();
				List<SqlFragment> ret = parseString(getStatement(
						aSubQuery, sIndexIf), sPattern, sDefaultReplaceString, aSubQueryIf);
				if (ret.size() == 1) {
					currentClause = ret.get(0);
					currentClause.setType(TYPE_IF);
					currentClause.setConditionName(sNome);
				} else {										
					currentClause.setSubClauses(ret);
					currentClause.setType(TYPE_IF);
					currentClause.setConditionName(sNome);
				}

				// impostiamo la clausola di query else
				List<String> aSubQueryElse=new ArrayList<String>();
				List<SqlFragment> retElse = parseString(getStatement(
						aSubQuery, sIndexElse), sPattern, sDefaultReplaceString, aSubQueryElse);
				if (retElse.size() == 1) {
					currentClause.setElseParameters(retElse.get(0).getParameters());
					currentClause.setElseSql(retElse.get(0).getSql(null));
				} else {
					currentClause.setElseSubClauses(retElse);
				}

				parsedQuery.add(currentClause);

			} else if (matcher.group(RegExGroup.GROUP_ARRAY) != null) {
				// -----------------------------------------------
				// ARRAY
				// -----------------------------------------------

				sReplaceString = sDefaultReplaceString;
				String sAttributo = StringUtility.nvl(
						matcher.group(RegExGroup.GROUP_ARRAY_ARGUMENT)).trim();
				String sFunzione = StringUtility.nvl(
						matcher.group(RegExGroup.GROUP_ARRAY)).trim();
				String sIndexVincolo = StringUtility.nvl(matcher
						.group(RegExGroup.GROUP_ARRAY_STATEMENT_ELSE));

				// creiamo parametro che deve essere considerato come un array
				SqlParameter param = createNewParameter(sFunzione,
						sAttributo);

				// prima di creare la clausola condizionata, la portiamo fuori.
				// TODO:qua
				matcher.appendReplacement(sb, "");
				sTemp = sb.toString(); // rimuoviamo i caratteri di invio etc
				// etc
				//TODO qua
				currentClause.setSql(currentClause.getSql(null) + sTemp + "");
				sb = new StringBuffer();

				currentClause = new SqlFragment(param);
				List<String> aSubQueryArray=new ArrayList<String>();
				List<SqlFragment> ret = parseString(getStatement(
						aSubQuery, sIndexVincolo), sPattern, sDefaultReplaceString, aSubQueryArray);
				
				if (ret.size() == 1) {
					currentClause = ret.get(0);
					currentClause.setType(TYPE_ARRAY);
					currentClause.setAttribute(param);
				} else {
					currentClause.setSubClauses(ret);
					currentClause.setType(TYPE_ARRAY);
					currentClause.setAttribute(param);
				}
				
				parsedQuery.add(currentClause);

			} else if (matcher.group(RegExGroup.GROUP_ATTRIBUTE) != null) {
				// -----------------------------------------------
				// Parametro
				// -----------------------------------------------
				// siamo innanzi ad un parametro con funzione
				sReplaceString = "?";
				String sProprieta = StringUtility.nvl(
						matcher.group(RegExGroup.GROUP_ATTRIBUTE_ARGUMENT))
						.trim();
				String sFunzione = StringUtility.nvl(
						matcher.group(RegExGroup.GROUP_ATTRIBUTE)).trim();

				SqlParameter sqlParameter = createNewParameter(sFunzione,
						sProprieta);

				currentClause.addParameter(sqlParameter); // aggiungiamo
				// parametro

				matcher.appendReplacement(sb, sReplaceString);
			} else if (matcher.group(RegExGroup.GROUP_VARIABLE) != null) {
				String sOldReplaceString = sReplaceString;
				sReplaceString = "{" + currentClause.getUsedVariables().size()
						+ "}";
				String sVariabile = StringUtility.nvl(
						matcher.group(RegExGroup.GROUP_VARIABILE_ARGUMENT))
						.trim();
				String sFunzione = StringUtility.nvl(
						matcher.group(RegExGroup.GROUP_VARIABLE)).trim();

				if (sVariabile.length() == 0) {
					sVariabile = sFunzione;
				}

				currentClause.getUsedVariables().add(sVariabile);

				matcher.appendReplacement(sb, sReplaceString);
				sReplaceString = sOldReplaceString;
			} else {
				throw (new DaoException("Condition not supported"));
			}

		}

		// se la currentClause non esiste o non è di tipo semplice, allora ne
		// creiamo un'altra di tipo semplice
		if (currentClause == null
				|| currentClause.getType() != SqlFragmentType.TYPE_SIMPLE) {
			sb = new StringBuffer();
			currentClause = new SqlFragment();

			parsedQuery.add(currentClause);
		}

		matcher.appendTail(sb);

		sTemp = sb.toString(); // rimuoviamo i caratteri di invio etc etc
		currentClause.setSql(sTemp);

		return parsedQuery;
	}

	/**
	 * Dati una funzione e un nome di attributo, crea un parametro. Il metodo
	 * gestisce anche la creazione dei parametri indicizzati
	 * 
	 * @param funzione
	 *            nome funzione
	 * @param proprieta
	 *            nome dell'attributo o dell'indice
	 * 
	 * @return parametro
	 */
	private static SqlParameter createNewParameter(String funzione,
			String proprieta) {
		SqlParameter sqlParameter = new SqlParameter();

		if (proprieta.length() == 0) {
			// non esiste la funzione
			proprieta = funzione; 
		} else {
			// impostiamo la funzione
			sqlParameter.setFunction(funzione); 
		}

		if (StringUtility.isInteger(proprieta)) {
			sqlParameter.setParameterIndex(Integer.parseInt(proprieta));
		} else {

			if (StringUtility.nvl(proprieta).toLowerCase().startsWith(USER_PARAMETER_PREFIX)) {
				proprieta = proprieta.substring(USER_PARAMETER_PREFIX.length());

				sqlParameter.setAttribute(proprieta);
				sqlParameter.setType(SqlParameterType.TYPE_USER_ATTRIBUTE);
			} else if (StringUtility.nvl(proprieta).toLowerCase().startsWith(CONFIG_PARAMETER_PREFIX)) {
				proprieta = proprieta.substring(CONFIG_PARAMETER_PREFIX.length());

				sqlParameter.setAttribute(proprieta);
				sqlParameter.setType(SqlParameterType.TYPE_CONFIG_ATTRIBUTE);
			} else {
				sqlParameter.setAttribute(proprieta);
			}
		}

		return sqlParameter;
	}


}
